1 module jupyter.wire.message; 2 3 4 import std.json: JSONValue; 5 6 /** 7 A message sent to the kernel. 8 See https://jupyter-client.readthedocs.io/en/stable/messaging.html#wire-protocol 9 */ 10 struct Message { 11 import std.json: JSONValue; 12 13 string[] identities; 14 MessageHeader header; 15 MessageHeader parentHeader; 16 // Not using asdf here because it's a pain to construct JSON objects with it 17 // and its serialisation doesn't buy anything since both metadata and content 18 // are free-form. 19 JSONValue metadata; 20 JSONValue content; 21 string[] extraRawData; 22 23 enum delimiter = "<IDS|MSG>"; 24 25 /** 26 Constructs the message from the strings sent to the control or shell 27 sockets. 28 */ 29 this(in string[] strings) @safe pure { 30 import asdf: deserialize; 31 import std.json: parseJSON; 32 import std.algorithm: countUntil; 33 34 const delimiterIndex = strings.countUntil(delimiter); 35 identities = strings[0 .. delimiterIndex].dup; 36 37 // TODO: verify signature 38 // hmac is delimiter + 1 39 40 () @trusted { 41 header = strings[delimiterIndex + 2].deserialize!MessageHeader; 42 parentHeader = strings[delimiterIndex + 3].deserialize!MessageHeader; 43 }(); 44 45 metadata = parseJSON(strings[delimiterIndex + 4]); 46 content = parseJSON(strings[delimiterIndex + 5]); 47 extraRawData = strings[delimiterIndex + 6 .. $].dup; 48 } 49 50 this(in Message other, in string msgType, JSONValue content) @safe { 51 identities = other.identities.dup; 52 this(other.header, msgType, content); 53 } 54 55 this(in MessageHeader parentHeader, in string msgType) @safe { 56 import std.json: parseJSON; 57 this(parentHeader, msgType, parseJSON(`{}`)); 58 } 59 60 this(in MessageHeader parentHeader, in string msgType, JSONValue content) @safe { 61 this.header = this.parentHeader = parentHeader; 62 this.header.msgType = msgType; 63 updateHeader; 64 this.content = content; 65 } 66 67 /** 68 Convert to a format suitable for sending over ZMQ 69 */ 70 string[] toStrings(in string key) @safe const { 71 return 72 identities.dup ~ 73 delimiter ~ 74 signature(key) ~ 75 header.toJsonString ~ 76 parentHeader.toJsonString ~ 77 metadata.toJsonString ~ 78 content.toJsonString ~ 79 extraRawData; 80 } 81 82 /** 83 Update header with a random uuid and setting the timestamp 84 */ 85 void updateHeader() @safe { 86 import std.datetime: DateTime, Clock; 87 import std.uuid: randomUUID; 88 89 header.date = (cast(DateTime)Clock.currTime).toISOExtString; 90 header.msgID = randomUUID.toString; 91 } 92 93 private string signature(in string key) @safe const { 94 import std.digest.hmac: hmac; 95 import std.digest.sha: SHA256; 96 import std.string: representation; 97 import std.array : appender; 98 import std.conv : toChars; 99 100 auto mac = hmac!SHA256(key.representation); 101 102 foreach(w; [header.toJsonString, parentHeader.toJsonString, toJsonString(metadata), toJsonString(content)]) 103 mac.put(w.representation); 104 105 ubyte[32] us = mac.finish; 106 auto cs = appender!string; 107 cs.reserve(64); 108 109 foreach(u; us[]) { 110 if (u <= 0xf) cs.put('0'); 111 cs.put(toChars!16(cast(uint) u)); 112 } 113 114 return cs.data; 115 } 116 } 117 118 119 struct MessageHeader { 120 import mir.serde: serdeKeys, serdeOptional; 121 122 @serdeOptional 123 @serdeKeys("msg_id") string msgID; 124 @serdeOptional 125 @serdeKeys("msg_type") string msgType; 126 @serdeOptional 127 @serdeKeys("username") string userName; 128 @serdeOptional string session; 129 @serdeOptional string date; 130 @serdeOptional 131 @serdeKeys("version") string protocolVersion; 132 133 static empty() { 134 import jupyter.wire.kernel : protocolVersion; 135 auto header = MessageHeader(); 136 header.protocolVersion = protocolVersion; 137 return header; 138 } 139 } 140 141 // can't be made a member function because `serializetoJson(this)` doesn't compile 142 private string toJsonString(MessageHeader header) @safe pure { 143 import asdf: serializeToJson; 144 import std.string: replace; 145 const prelim = header.msgID is null ? "{}" : () @trusted { return serializeToJson(header); }(); 146 return prelim.replace("null", `""`); 147 } 148 149 // can't be pure due to JSONValue.toString 150 private string toJsonString(in JSONValue json) @safe { 151 import std.json: JSONType; 152 return json.type == JSONType.null_ ? `{}` : json.toString; 153 } 154 155 156 Message statusMessage(MessageHeader header, in string status) @safe { 157 import std.json: JSONValue; 158 JSONValue content; 159 content["execution_state"] = status; 160 auto ret = pubMessage(header, "status", content); 161 return ret; 162 } 163 164 165 Message pubMessage(MessageHeader header, in string msgType, JSONValue content, JSONValue metadata = JSONValue()) @safe { 166 import std.json : JSONType, parseJSON; 167 auto ret = Message(header, msgType, content); 168 ret.identities = [msgType]; 169 if (metadata.type == JSONType.object) 170 ret.metadata = metadata; 171 else 172 ret.metadata = parseJSON(`{}`); 173 return ret; 174 } 175 176 177 struct CompleteResult { 178 string[] matches; 179 long cursorStart; 180 long cursorEnd; 181 string[string] metadata; 182 string status; 183 } 184 185 186 Message completeMessage(in Message requestMessage, in CompleteResult result) @safe { 187 import std.json: JSONValue; 188 189 JSONValue content; 190 content["matches"] = result.matches; 191 content["cursor_start"] = result.cursorStart; 192 content["cursor_end"] = result.cursorEnd; 193 content["metadata"] = result.metadata; 194 content["status"] = result.status; 195 196 return Message(requestMessage, "complete_reply", content); 197 } 198 199 Message commOpenMessage(in string commId, in string targetName, JSONValue data = JSONValue(), JSONValue metadata = JSONValue()) @safe { 200 201 JSONValue content; 202 content["comm_id"] = commId; 203 content["target_name"] = targetName; 204 content["data"] = data; 205 206 return pubMessage(MessageHeader.empty(), "comm_open", content, metadata); 207 } 208 209 Message commCloseMessage(in Message requestMessage) @safe { 210 211 JSONValue content; 212 content["comm_id"] = requestMessage.content["comm_id"]; 213 214 return pubMessage(requestMessage.header, "comm_close", content); 215 } 216 217 Message commCloseMessage(in string commId, JSONValue data = JSONValue()) @safe { 218 219 JSONValue content; 220 content["comm_id"] = commId; 221 content["data"] = data; 222 223 return pubMessage(MessageHeader.empty(), "comm_close", content); 224 } 225 226 Message commMessage(in string commId, JSONValue data = JSONValue(), JSONValue metadata = JSONValue()) @safe { 227 228 JSONValue content; 229 content["comm_id"] = commId; 230 content["data"] = data; 231 232 return pubMessage(MessageHeader.empty(), "comm_msg", content, metadata); 233 } 234 235 Message displayDataMessage(JSONValue data, JSONValue metadata = JSONValue()) @safe { 236 237 import std.json : JSONType, parseJSON; 238 JSONValue content; 239 content["data"] = data; 240 content["metadata"] = metadata.type == JSONType.object ? metadata : parseJSON(`{}`); 241 242 return pubMessage(MessageHeader.empty(), "display_data", content); 243 }